--- import { type CollectionEntry, getCollection } from "astro:content"; import { render } from "astro:content"; import Translations from "@components/Translations.astro"; import KeywordsList from "@components/organisms/KeywordsList.astro"; import Citations from "@components/Citations.astro"; import Signature from "@components/templates/signature/Signature.astro"; import CopyrightNotice from "@components/templates/CopyrightNotice.astro"; import { verifier as verifierPrototype } from "@lib/pgp/verify"; import { fromPosts, getSigners, getSignersIDs, isTranslation, licenseNotice, licenseURL, } from "@lib/collection/helpers"; import { defined, get, transform } from "@utils/anonymous"; import Authors from "@components/templates/Authors.astro"; import Base from "@layouts/Base.astro"; import type { GetStaticPaths, InferGetStaticParamsType, InferGetStaticPropsType, } from "astro"; import DateTime from "@components/organisms/Date.astro"; import { getUserIDsFromKey } from "@lib/pgp/user"; import type { PublicKey, UserIDPacket } from "openpgp"; import type { BlogPosting, Person } from "@lib/collection/types"; import { type MicroEntry, Original, type OriginalEntry, Translation, } from "@lib/collection/schemas"; import { getEntry } from "astro:content"; import { getEntries } from "astro:content"; import readingTime from "reading-time"; import { fileCreationCommitDate } from "@lib/git/log"; export const getStaticPaths = (async (): Promise< { params: { slug: string }; props: CollectionEntry<"blog">; }[] > => { const posts = await getCollection("blog"); return posts.map((post) => ({ params: { slug: post.id }, props: post, })); }) satisfies GetStaticPaths; type Params = InferGetStaticParamsType; type Props = InferGetStaticPropsType; let post: Props | undefined = Astro.props; const verifier = await verifierPrototype.then((x) => x.clone()); const signers: Map< string, { signer: Awaited>[number]; users: UserIDPacket[]; key: PublicKey; } > = new Map(); // Add signers public keys to keyring for (const signer of await getSigners(post)) { const { data } = signer.entity; const key = await verifier.addKeyFromArmor(data.publickey.armor); signers.set(key.getFingerprint(), { signer, users: getUserIDsFromKey(undefined, key), key, }); } const createPerson = ( { signer, users }: typeof signers extends Map ? V : never, ): Person | undefined => ({ "@type": "Person", "@id": signer.entity.id, // TODO: URL name: users.find(({ name }) => name.length > 0)?.name, url: signer.entity.data.websites, email: users.find(({ email }) => email.length > 0)?.email, }); const signersValues = Array.from(signers.values()); const author: Person | undefined = transform( signersValues.find(({ signer }) => signer.role === "author"), (x) => x !== undefined ? createPerson(x) : undefined, ); const coauthors: Person[] = signersValues.filter(({ signer }) => signer.role === "co-author" ).map(createPerson).filter(defined); const translators: Person[] = signersValues.filter(({ signer }) => signer.role === "translator" ).map(createPerson).filter(defined); const { id, data, rendered, body, filePath } = post; const path = new URL(`file://${Deno.cwd()}/${filePath}`); const verification = post.filePath !== undefined ? await verifier.verify([path]) : undefined; const commit = await verification?.commit; const { title, lang, dateCreated, dateUpdated, license } = data; let original: OriginalEntry | MicroEntry; try { const { translationOf } = Translation.parse(post); const maybeOriginal = await getEntry(translationOf) as | OriginalEntry | MicroEntry | undefined; if (maybeOriginal === undefined) { throw new Error(`Original post not found for ${id}`); } original = maybeOriginal; const { author: [originalAuthors], "co-author": originalCoauthors } = getSignersIDs(original); const originalAuthor = originalAuthors?.[0]; if ( (author !== undefined && author["@id"] !== originalAuthor) || !new Set(coauthors).isSubsetOf(new Set(originalCoauthors)) ) { throw new Error( `Post ${id} has mismatched (co-)authors from original post ${original.id}`, ); } for (const { "@id": t } of translators) { if ( originalAuthor === t || originalCoauthors.includes(t) ) { throw new Error( `Translator ${t} in ${id} is already a (co-)author in original post`, ); } } } catch { original = post as OriginalEntry | MicroEntry; if (signersValues.some(({ signer }) => signer.role === "translator")) { throw new Error( `Post ${id} is not a translation but has translators defined`, ); } } const translationsSet = await fromPosts( isTranslation, (x) => new Set( x.filter(({ data }) => data.translationOf.id === original.id).map( get("id"), ), ), ); translationsSet.add(original.id); const translations = await getEntries( Array.from(translationsSet).map((id) => ({ collection: original.collection, id, })), ); const reading = body ? readingTime(body, {}) : undefined; const minutes = reading === undefined ? undefined : Math.ceil(reading.minutes); const estimative = minutes === undefined ? undefined : new Intl.DurationFormat(lang, { style: "long", }).format({ hours: Math.floor(minutes / 60), minutes: minutes % 60 }); const duration = minutes === undefined ? undefined : `PT${Math.floor(minutes / 60) > 0 ? Math.floor(minutes / 60) + "H" : ""}${ minutes % 60 > 0 ? minutes % 60 + "M" : "" }`; const linkedData: BlogPosting & { "@context": "https://schema.org" } = { "@context": "https://schema.org", "@type": "BlogPosting", "@id": Astro.url.href, url: Astro.url.href, headline: title, name: title, abstract: "description" in data ? data.description : undefined, alternativeHeadline: "subtitle" in data ? data.subtitle : undefined, inLanguage: lang, workTranslations: translations.filter((post) => post.id !== id && post.id !== original.id ).map(({ id, data }) => ({ "@type": "BlogPosting", "@id": new URL(`blog/read/${id}`, Astro.site).href, url: new URL(`blog/read/${id}`, Astro.site).href, headline: data.title, name: data.title, inLanguage: data.lang, dateCreated: data.dateCreated.toISOString(), license: licenseURL(data.license)?.href, translator: data.signers.filter(({ role }) => role === "translator") .map(( { entity }, ): Person => ({ "@type": "Person", "@id": entity.id, })), }) as BlogPosting ), translationOfWork: original.id !== post.id ? { "@type": "BlogPosting", "@id": new URL(`blog/read/${original.id}`, Astro.site).href, url: new URL(`blog/read/${original.id}`, Astro.site).href, headline: original.data.title, name: original.data.title, inLanguage: original.data.lang as string, dateCreated: original.data.dateCreated.toISOString(), license: licenseURL(original.data.license)?.href, } as BlogPosting : undefined, // TODO: version author, contributor: coauthors, translator: translators, dateCreated: dateCreated.toISOString(), dateModified: dateUpdated?.toISOString(), datePublished: await fileCreationCommitDate(path).then((date) => date?.toISOString() ), timeRequired: duration, wordCount: reading?.words, articleBody: rendered?.html ?? body, text: rendered?.html ?? body, keywords: original.data.keywords, citation: await transform( Original.safeParse(original.data).data, async (o) => { if (o === undefined) return o; const related = await getEntries(o.relatedPosts); return related.map(({ data }): BlogPosting => ({ "@type": "BlogPosting", "@id": new URL(`blog/read/${id}`, Astro.site).href, url: new URL(`blog/read/${id}`, Astro.site).href, headline: data.title, name: data.title, inLanguage: data.lang, dateCreated: data.dateCreated.toISOString(), license: licenseURL(data.license)?.href ?? undefined, })); }, ), // TODO: citation V.S. mentions mentions: await transform( Original.safeParse(original.data).data, async (o) => { if (o === undefined) return o; const related = await getEntries(o.relatedPosts); return related.map(({ data }): BlogPosting => ({ "@type": "BlogPosting", "@id": new URL(`blog/read/${id}`, Astro.site).href, url: new URL(`blog/read/${id}`, Astro.site).href, headline: data.title, name: data.title, inLanguage: data.lang, dateCreated: data.dateCreated.toISOString(), license: licenseURL(data.license)?.href ?? undefined, })); }, ), // TODO: citation V.S. mentions copyrightHolder: [author, ...coauthors, ...translators].filter(defined), copyrightNotice: licenseNotice(license, { title, holders: signersValues.map(({ users }) => { const user = users?.[0]; if (user === undefined) return undefined; const { name, email } = user; return (name.length > 0 && email.length > 0) ? { name, email } : undefined; }).filter(defined), years: new Array( // TODO: get years where there were commits (dateUpdated?.getFullYear() ?? dateCreated.getFullYear()) - dateCreated.getFullYear() + 1, ).fill(dateCreated.getFullYear()).map((x, i) => x + i), }, lang), copyrightYear: dateCreated.getFullYear(), creativeWorkStatus: "Published", encodingFormat: "text/html", isAccessibleForFree: true, license: licenseURL(license)?.href ?? undefined, publisher: transform(commit?.committer, (commiter) => { if (commiter === undefined) return undefined; const { name, email } = commiter; return { "@type": "Person", name, email, }; }), }; const { Content } = await render(post); post = undefined; ---

{linkedData.headline}

{ linkedData.alternativeHeadline && (

{linkedData.alternativeHeadline}

) }
{ linkedData.abstract && (

Resumo

{ linkedData.abstract.split(new RegExp("\\s{2,}")) .map(( x, ) =>

{x}

) }
) } {verification && }
{ verification?.verifications && ( ) }
Data de criação
{ linkedData.dateModified && (
Última atualização
) } { linkedData.locationCreated && (
Local de criação
{linkedData.locationCreated.name}
) } { linkedData.wordCount && linkedData.timeRequired && ( <>
Tempo de leitura estimado
~ {estimative} (palavras: {linkedData.wordCount})
) }
{ linkedData.keywords !== undefined && linkedData.keywords.length > 0 && (
) } { linkedData.citation !== undefined && ( ) } x + i)} {license} />